'use strict'

const fs = require('fs')
const path = require('path')

const sg = require('simple-git')
const fse = require('fs-extra')
const userHome = require('user-home')
const inqurier = require('inquirer')
const terminalLink = require('terminal-link')

const log = require('@lenovo-cli/log')
const { readFile, writeFile, spinnerStart } = require('@lenovo-cli/utils')
const request = require('@lenovo-cli/request')
const Github = require('./class/Github')
const Gitee = require('./class/Gitee')
const CloudBuild = require('@lenovo-cli/cloudbuild')
const inquirer = require('inquirer')
const semver = require('semver')

const DEFAULT_CLI_HOME = '.lenovo-cli'
const GIT_SERVER_FILE = '.git_server' // 通过定义缓存文件.git_server储存git server信息
const GIT_TOKEN_FILE = '.git_token' // 通过定义缓存文件.git_token储存git token信息
const GIT_ROOT_DIR = '.git'
const GIT_OWN_FILE = '.git_own' // 储存类型，是个人还是组织
const GIT_LOGIN_FILE = '.git_login' // 存储登录名
const GIT_PUBLISH_FILE = '.git_publish'
const BRANCH_RELEASE = 'release'
const BRANCH_DEV = 'dev'
const TEMPLATE_TEMP_DIR = 'oss' // oss模版文件储存目录

const GIT_OWNER_TYPE = [
  {
    name: '个人',
    value: 'user',
  },
  {
    name: '组织',
    value: 'org',
  },
]
const GIT_OWNER_TYPE_ONLY = [
  {
    name: '个人',
    value: 'user',
  },
]
const GIT_PUBLISH_TYPE = [
  {
    name: 'OSS',
    value: 'oss',
  },
]

// Git实例中包含项目名称、项目版本、项目目录等
class Git {
  constructor(
    { pkgName, pkgVersion, pkgDir },
    {
      update = false,
      token = false,
      type = false,
      buildCmd = '',
      prod = false,
      sshUser = '',
      sshIp = '',
      sshPath = '',
    }
  ) {
    this.pkgName = pkgName // 项目名称
    this.pkgVersion = pkgVersion // 项目版本
    this.pkgDir = pkgDir // 项目目录
    this.git = sg(pkgDir) // simple git 实例
    this.gitServer = null // GitServer实例
    this.homePath = null // 本地缓存目录
    this.user = null // 用户信息
    this.orgs = null // 用户所属组织列表
    this.owner = null // 远程仓库类型
    this.login = null // 远程仓库登录名
    this.repo = null // 远程仓库信息
    this.branch = null // 本地开发分支
    this.remote = null // 远程仓库git地址
    this.gitPublish = null // 静态资源服务器类型

    this.update = update // 是否更新远程仓库
    this.token = token // 是否强制刷新远程仓库token
    this.type = type // 是否强制刷新远程仓库类型
    this.buildCmd = buildCmd // 构建命令
    this.prod = prod // 是否正式发布
    this.sshUser = sshUser
    this.sshIp = sshIp
    this.sshPath = sshPath
    log.verbose('ssh config', this.sshUser, this.sshIp, this.sshPath)
  }

  async prepare() {
    log.info(`---------- git flow 流程开始----------`)
    // 检查缓存主目录
    this.checkHomePath()
    // 检查用户远程仓库类型
    await this.checkGitServer()
    // 获取远程仓库token
    await this.checkGitToken()
    // 获取远程仓库用户和组织信息， 注：用户仓库和组织仓库的api是不一样的
    await this.getUserAndOrgs()
    // 确认远程仓库类型 用户仓库还是组织仓库
    await this.checkGitOwner()
    // 创建远程仓库
    await this.checkRepo()
    // 检查并创建 .gitignore 文件
    this.checkFileGitignore()
    // 本地仓库初始化
    await this.init()
  }

  checkFileGitignore() {
    const gitIgnore = path.resolve(this.pkgDir, '.gitignore')
    // 当.gitignore文件不存在时，生成文件
    if (!fs.existsSync(gitIgnore)) {
      writeFile(
        gitIgnore,
        `.DS_Store
node_modules
/dist
.vscode
.idea
.prettierrc
npm-debug.log*
yarn-debug.log*
yarn-error.log*      
      `
      )
      log.success('.gitignore 文件写入成功！')
    }
  }

  async checkRepo() {
    let repo = await this.gitServer.getRepo(this.login, this.pkgName)
    //tag 当前项目在远程仓库不存在的时候，就创建远程仓库
    if (!repo) {
      let spinner = spinnerStart('开始创建远程仓库...')
      try {
        // 个人仓库
        if (this.owner === 'user') {
          repo = await this.gitServer.createRepo(this.pkgName)
        } else {
          // 组织仓库
          repo = await this.gitServer.createOrgRepo(this.pkgName, this.login)
        }
      } catch (e) {
        log.error(e)
      } finally {
        spinner.stop(true)
      }
      if (repo) {
        log.success('远程仓库创建成功！')
      } else {
        throw new Error('远程仓库创建失败！')
      }
    } else {
      // 远程仓库存在
      log.success('远程仓库信息获取成功！')
    }
    this.repo = repo
  }

  async checkGitOwner() {
    const ownerPath = this.createPath(GIT_OWN_FILE)
    const loginPath = this.createPath(GIT_LOGIN_FILE)
    let owner = readFile(ownerPath)
    let login = readFile(loginPath)
    if (!owner || !login || this.type) {
      owner = (
        await inqurier.prompt({
          type: 'list',
          name: 'owner',
          message: '请选择远程仓库类型',
          default: 'user',
          choices: this.orgs.length > 0 ? GIT_OWNER_TYPE : GIT_OWNER_TYPE_ONLY,
        })
      ).owner
      if (owner === 'user') {
        login = this.user.login // 从user中拿到登录名
      } else {
        login = (
          await inquirer.prompt({
            type: 'list',
            name: 'login',
            message: '请选择',
            choices: this.orgs.map(item => ({
              name: item.login,
              value: item.login,
            })),
          })
        ).login
      }

      // 写入缓存
      writeFile(ownerPath, owner)
      writeFile(loginPath, login)

      log.success('远程仓库类型写入成功！', `${owner} -> ${ownerPath}`)
      log.success('用户登录名获取成功！', `${login} -> ${loginPath}`)
    } else {
      log.success('远程仓库类型获取成功！', owner)
      log.success('用户登录名获取成功！', login)
    }

    this.owner = owner
    this.login = login
  }

  async getUserAndOrgs() {
    this.user = await this.gitServer.getUser()
    if (!this.user) {
      throw new Error('用户信息获取失败！')
    }
    this.orgs = await this.gitServer.getOrg(this.user.login)
    if (!this.orgs) {
      throw new Error('用户组织信息获取失败！')
    }
    log.success(this.gitServer.type + '用户和组织信息获取成功！')
  }

  async checkGitServer() {
    // 创建完整的.git_server文件路径 /Users/liyu/.lenovo-cli/.git/.git_server
    const gitServerPath = this.createPath(GIT_SERVER_FILE)
    let gitServer = readFile(gitServerPath)
    // 若gitserver文件不存在，或者用户输入publish命令时增加了--update时，获取用户选择结果写入文件
    if (!gitServer || this.update) {
      gitServer = (
        await inqurier.prompt({
          type: 'list',
          name: 'gitServer',
          message: '请选择git平台',
          default: 'gitee',
          choices: [
            {
              name: 'github',
              value: 'github',
            },
            {
              name: 'gitee',
              value: 'gitee',
            },
          ],
        })
      ).gitServer
      writeFile(gitServerPath, gitServer)
      log.success('git server 写入成功:', `${gitServer} -> ${gitServerPath}`)
    } else {
      log.success('git server获取成功:', gitServer)
    }
    this.gitServer = this.createGitServer(gitServer)
    if (!this.gitServer) {
      throw new Error('GitServer 初始化失败！')
    }
  }

  async checkGitToken() {
    const tokenPath = this.createPath(GIT_TOKEN_FILE)
    let token = readFile(tokenPath)
    if (!token || this.token) {
      // token 不存在的时候给出提示
      log.warn(
        `${this.gitServer.type} token未生成！` +
          terminalLink('生成链接:', this.gitServer.getTokenUrl())
      )

      // 让用户贴入token
      token = (
        await inqurier.prompt({
          type: 'password',
          name: 'token',
          message: '请复制token到这里',
          default: '',
        })
      ).token
      writeFile(tokenPath, token)
      log.success('token写入成功', `${token} -> ${tokenPath}`)
    } else {
      log.success('token获取成功', tokenPath)
    }
    this.token = token
    this.gitServer.setToken(token)
  }

  createGitServer(gitServer) {
    if (gitServer === 'github') {
      return new Github()
    } else if (gitServer === 'gitee') {
      return new Gitee()
    }
    return null
  }

  createPath(file) {
    const rootDir = path.resolve(this.homePath, GIT_ROOT_DIR)
    const filePath = path.resolve(rootDir, file)
    fse.ensureDirSync(rootDir)
    return filePath
  }

  checkHomePath() {
    if (!this.homePath) {
      if (process.env.CLI_HOME_PATH) {
        this.homePath = process.env.CLI_HOME_PATH
      } else {
        this.homePath = path.resolve(userHome, DEFAULT_CLI_HOME)
      }
    }
    log.verbose('home', this.homePath)
    fse.ensureDirSync(this.homePath) // 确保用户目录可用
    if (!fs.existsSync(this.homePath)) {
      throw new Error('用户主目录获取失败！')
    }
  }

  async init() {
    if (await this.getRemote()) {
      return
    }
    await this.gitInitAndConnectRemote() // 调用 simple-git 库的 api，进行本地 git init 初始化操作并与远程仓库绑定
    await this.gitInitCommit() // git 初始化提交
  }

  async commit() {
    // 生成开发分支
    await this.getCorrectVersion()
    // 检查stash区
    await this.checkStash()
    // 检查代码冲突
    await this.checkCodeConflicted()
    // 检查未提交代码
    await this.checkCodeNotCommitted()
    // 切换到开发分支
    await this.checkoutBranch(this.branch)
    // 合并远程master分支和开发分支代码（把远程master分支合并到本地开发分支）
    await this.pullRemoteMasterAndBranch()
    // 将开发分支推送到远程仓库
    await this.pushRemoteRepo(this.branch)
    log.info(`---------- git flow 流程结束 ----------`)
  }

  async publish() {
    log.info(`---------- 云构建与发布流程开始 ----------`)
    await this.preparePublish()
    const cloudbuild = new CloudBuild(this, {
      buildCmd: this.buildCmd,
      type: this.gitPublish, // 静态资源服务器类型
      prod: this.prod,
    })
    //tag await cloudbuild.prepare() // 查询ali oss上是否已存在项目文件
    await cloudbuild.init() // init中原本是一些websocket异步操作，所以通过需要通过promise改造进行处理
    const ret = await cloudbuild.build()
    if (ret) {
      await this.uploadTemplate()
    }

    if (this.prod && ret) {
      // 若是开发模式，进行打tag
      await this.checkTag()
      await this.checkoutBranch('master') // 切换分支到 master
      await this.mergeBranchToMaster() // 将开发分支代码合并到master
      await this.pushRemoteRepo('master') // 推送至远程master
      await this.deleteLocalBranch() // 删除本地开发分支
      await this.deleteRemoteBranch() // 删除远程开发分支
    }
  }

  async deleteLocalBranch() {
    log.info('开始删除本地开发分支', this.branch)
    await this.git.deleteLocalBranch(this.branch)
    log.success('删除本地分支成功', this.branch)
  }

  async deleteRemoteBranch() {
    log.info('开始删除远程分支', this.branch)
    await this.git.push(['origin', '--delete', this.branch])
    log.success('删除远程分支成功', this.branch)
  }

  async mergeBranchToMaster() {
    log.info('开始合并代码', `[${this.branch}] -> [master]`)
    await this.git.mergeFromTo(this.branch, 'master')
    log.success('代码合并成功', `[${this.branch}] -> [master]`)
  }

  async checkTag() {
    // 检查项目是否已tag
    log.info('获取远程 tag 列表')
    const tag = `${BRANCH_RELEASE}/${this.pkgVersion}`
    const tagList = await this.getRemoteBranchList(BRANCH_RELEASE)
    if (tagList.includes(this.pkgVersion)) {
      log.success('远程 tag 已存在', tag)
      await this.git.push(['origin', `:refs/tags/${tag}`]) // git push origin :refs/tags/release1.1.1
      log.success('远程 tag 已删除', tag)
    }

    const localTagList = await this.git.tags()
    if (localTagList.all.includes(tag)) {
      // 本地所有的tags
      log.success('本地 tag 已存在', tag)
      await this.git.tag(['-d', tag])
      log.success('远程 tag 已删除', tag)
    }

    await this.git.addTag(tag)
    log.success('本地 tag 创建成功', tag)
    await this.git.pushTags('origin')
    log.success('远程 tag 推送成功', tag)
  }

  async uploadTemplate() {
    const TEMPLATE_FILE_NAME = 'index.html'
    if (this.sshUser && this.sshIp && this.sshPath) {
      log.info('开始下载模版文件')
      const ossTemplateFile = await request({
        url: 'oss/get',
        params: {
          name: this.pkgName,
          type: this.prod ? 'dev' : 'dev', //tag 暂无prod环境
          file: TEMPLATE_FILE_NAME,
        },
      })
      if (ossTemplateFile.code === 0 && ossTemplateFile.data) {
        ossTemplateFile = ossTemplateFile.data
      }
      let response = await request({
        url: ossTemplateFile.url,
      })
      if (response) {
        const ossTempDir = path.resolve(
          this.homePath,
          TEMPLATE_TEMP_DIR,
          `${this.pkgName}@${this.pkgVersion}`
        )
        if (!fs.existsSync(ossTempDir)) {
          // 不存在的话，创建目录
          fse.mkdirpSync(ossTempDir) // 加上p，所有中间未创建的目录都会自动创建
        } else {
          fse.emptyDirSync(ossTempDir)
        }

        const templateFilePath = path.resolve(ossTempDir, TEMPLATE_FILE_NAME)
        fse.createFileSync(templateFilePath) // 创建文件
        fs.writeFileSync(templateFilePath, response) // 写入文件
        log.success('模版文件下载成功', templateFilePath)
        log.info('开始上传模版文件至服务器')
        const uploadCmd = `scp -r ${templateFilePath} ${this.sshUser}@${this.sshIp}:${this.sshPath}`
        log.verbose('uploadCmd', uploadCmd)
        const ret = require('child_process').execSync(uploadCmd)
        log.success('模版文件上传成功！')
        fse.emptydirSync(ossTempDir)
      }
    }
  }

  async preparePublish() {
    log.info('开始进行云构建前的代码检查')
    const pkg = this.getPackageJson()
    if (this.buildCmd) {
      // --buildCmd 存在的话进行命令分解判断
      const buildCmdArr = this.buildCmd.split(' ')
      if (buildCmdArr[0] !== 'npm' && buildCmdArr[0] !== 'cnpm') {
        throw new Error('必须使用npm或cnpm进行build命令！')
      }
    } else {
      // 不传--buildCmd时，自动设置一个build命令： npm run build
      this.buildCmd = 'npm run build'
    }

    const buildCmdArr = this.buildCmd.split(' ')
    const lastCmd = buildCmdArr[buildCmdArr.length - 1]
    if (!pkg.scripts || !Object.keys(pkg.scripts).includes(lastCmd)) {
      throw new Error(this.buildCmd + ' 命令不存在！')
    }

    log.success('代码预检查通过')

    //tag 发布oss类型选择
    // const gitPublishPath = this.createPath(GIT_PUBLISH_FILE)
    // let gitPublish = readFile(gitPublishPath)
    // if (!gitPublish) {
    //   gitPublish = (
    //     await inquirer.prompt({
    //       type: 'list',
    //       choices: GIT_PUBLISH_TYPE,
    //       message: '请选择要上传代码的平台',
    //       name: 'gitPublish',
    //     })
    //   ).gitPublish
    //   writeFile(gitPublishPath, gitPublish)
    //   log.success(
    //     'git publish类型写入成功',
    //     `${gitPublish} -> ${gitPublishPath}`
    //   )
    // } else {
    //   log.success('git publish 类型获取成功', gitPublish)
    // }
    // this.gitPublish = gitPublish
  }

  getPackageJson() {
    const pkgPath = path.resolve(this.pkgDir, 'package.json')
    if (!fs.existsSync(pkgPath)) {
      throw new Error(`package.json 不存在！源码目录：${this.pkgDir}`)
    }
    return fse.readJsonSync(pkgPath)
  }

  async pullRemoteMasterAndBranch() {
    log.info(`合并 [master] -> [${this.branch}]`)
    await this.pullRemoteRepo('master')
    await this.checkCodeConflicted() // 合并之后要检查代码冲突
    log.info('检查远程开发分支')
    const remoteBranchList = await this.getRemoteBranchList()
    if (remoteBranchList.indexOf(this.pkgVersion) >= 0) {
      // 存在远程开发分支时，进行代码拉取
      log.info(`合并 [${this.branch}] -> [${this.branch}]`)
      await this.pullRemoteRepo(this.branch)
      log.success(`合并远程 [${this.branch}] 分支代码成功`)
      await this.checkCodeConflicted()
    } else {
      log.success(`不存在远程分支 [${this.branch}]`)
    }
  }

  async checkoutBranch(branch) {
    const localBranchList = await this.git.branchLocal() // git branch命令
    if (localBranchList.all.indexOf(branch) >= 0) {
      // 当分支存在的时候，直接切换
      await this.git.checkout(branch)
    } else {
      // 当分支不存在的时候，创建分支并切换
      await this.git.checkoutLocalBranch(branch) // git checkout -b ...
    }
    log.success(`分支切换到${branch}`)
  }

  async checkStash() {
    log.info('检查stash记录')
    const stashList = await this.git.stashList()
    if (stashList.all.length > 0) {
      await this.git.stash(['pop'])
      log.success('stash pop成功！')
    }
  }

  async getCorrectVersion() {
    // 获取远程发布分支 规范：远程分支 release/x.y.z, 开发分支 dev/x.y.z， 版本号递增： major/minor/patch
    log.info('获取代码分支')
    const remoteBranchList = await this.getRemoteBranchList(BRANCH_RELEASE)
    let releaseVersion = null // 最新的远程分支
    if (remoteBranchList && remoteBranchList.length > 0) {
      releaseVersion = remoteBranchList[0]
    }
    log.verbose('线上最新release版本号', releaseVersion)
    // 生成本地开发分支
    const devVersion = this.pkgVersion
    if (!releaseVersion) {
      this.branch = `${BRANCH_DEV}/${devVersion}`
    } else if (semver.gt(this.pkgVersion, releaseVersion)) {
      log.info('当前版本大于线上最新版本', `${devVersion} >= ${releaseVersion}`)
      this.branch = `${BRANCH_DEV}/${devVersion}`
    } else {
      log.info('当前线上版本大于本地版本', `${releaseVersion} > ${devVersion}`)
      const incType = (
        await inquirer.prompt({
          type: 'list',
          name: 'incType',
          message: '自动升级版本，请选择升级版本类型',
          default: 'patch',
          choices: [
            {
              // server.inc自动升级版本号
              name: `小版本（${releaseVersion} -> ${semver.inc(
                releaseVersion,
                'patch'
              )}）`,
              value: 'patch',
            },
            {
              name: `中版本（${releaseVersion} -> ${semver.inc(
                releaseVersion,
                'minor'
              )}）`,
              value: 'minor',
            },
            {
              name: `大版本（${releaseVersion} -> ${semver.inc(
                releaseVersion,
                'major'
              )}）`,
              value: 'major',
            },
          ],
        })
      ).incType
      // 根据所选类型，生成升级后的版本号
      const incVersion = semver.inc(releaseVersion, incType)
      this.branch = `${BRANCH_DEV}/${incVersion}`
      this.pkgVersion = incVersion // 升级本地版本号
    }
    log.verbose('本地开发分支', this.branch)
    // 将升级后的版本号写入本地package.json文件
    this.syncVersionToPkgJson()
  }

  syncVersionToPkgJson() {
    const pkg = fse.readJSONSync(`${this.pkgDir}/package.json`)
    if (pkg && pkg.version !== this.pkgVersion) {
      pkg.version = this.pkgVersion
      fse.writeJSONSync(`${this.pkgDir}/package.json`, pkg, { spaces: 2 }) // 写入文件时两行缩进
    }
  }

  async getRemoteBranchList(type) {
    // git ls-remote --refs
    const remoteList = await this.git.listRemote(['--refs'])
    let reg
    if (type === BRANCH_RELEASE) {
      reg = /.+?refs\/tags\/release\/(\d+\.\d+\.\d+)/g
    } else {
      reg = /.+?refs\/heads\/dev\/(\d+\.\d+\.\d+)/g
    }
    return remoteList
      .split('\n')
      .map(remote => {
        const match = reg.exec(remote)
        reg.lastIndex = 0 // 远程分支会存在多个，所以为避免正则第一次获取到之后就停止匹配，需要设置reg.lastIndex
        if (match && semver.valid(match[1])) {
          return match[1]
        }
      })
      .filter(x => x) // 通过filter去掉undefined项
      .sort((a, b) => {
        // 再进行版本号排序
        if (semver.lte(b, a)) {
          if (a === b) return 0
          return -1
        }
        return 1
      })
  }

  async gitInitCommit() {
    await this.checkCodeConflicted() // 检查代码冲突
    await this.checkCodeNotCommitted() // 检查本地代码是否有未提交，有的话进行初始化本地提交代码

    // 当远程存在master分支时
    if (await this.checkRemoteMaster()) {
      await this.pullRemoteRepo('master', {
        '--allow-unrelated-histories': null, // 让没有关系的远程代码和本地代码进行合并
      })
    } else {
      // 当远程不存在master时，推送到远程master
      this.pushRemoteRepo('master')
    }
  }

  async pullRemoteRepo(branchName, options) {
    log.info(`同步远程 ${branchName} 分支代码`)
    await this.git.pull('origin', branchName, options).catch(err => {
      log.error(err.message)
    })
  }

  async checkRemoteMaster() {
    // 判断远程master分支是否存在: 利用 git ls-remote s命令实现  ex: fdaf4535gdsgsg435353fgfdsg refs/heads/master
    return (
      (await this.git.listRemote(['--refs'])).indexOf('refs/heads/master') >= 0
    )
  }

  async pushRemoteRepo(branchName) {
    log.info(`推送代码至 ${branchName} 分支`)
    await this.git.push('origin', branchName)
    log.success('推送代码成功')
  }

  async checkCodeConflicted() {
    log.info('代码冲突检查')
    const status = await this.git.status() // status.conflicted数组
    if (status.conflicted.length > 0) {
      throw new Error('当前代码存在冲突，请手动处理合并后再试！')
    }
    log.success('代码冲突检查通过')
  }

  async checkCodeNotCommitted() {
    const status = await this.git.status() // status.not_added数组
    // 本地有未提交的话进行本地初始化代码提交
    if (
      status.not_added.length > 0 ||
      status.created.length > 0 ||
      status.deleted.length > 0 ||
      status.modified.length > 0 ||
      status.renamed.length > 0
    ) {
      {
        await this.git.add(status.not_added)
        await this.git.add(status.created)
        await this.git.add(status.deleted)
        await this.git.add(status.modified)
        await this.git.add(status.renamed)
        let message
        while (!message) {
          message = (
            await inquirer.prompt({
              type: 'text',
              name: 'message',
              message: '请输入commit信息：',
            })
          ).message
        }
        await this.git.commit(message)
        log.success('本次commit提交成功！')
      }
    }
  }

  async getRemote() {
    const gitPath = path.resolve(this.pkgDir, GIT_ROOT_DIR)
    this.remote = this.gitServer.getRemote(this.login, this.pkgName)
    // 判断目录中是否有 .git 文件
    if (fs.existsSync(gitPath)) {
      log.success('git已完成初始化')
      return true
    }
    return false
  }

  async gitInitAndConnectRemote() {
    log.notice('执行 git 初始化')
    await this.git.init(this.pkgDir)
    log.notice('添加 git remote')
    const remotes = await this.git.getRemotes()
    log.verbose('git remotes', remotes)
    if (!remotes.find(item => item.name === 'origin')) {
      await this.git.addRemote('origin', this.remote) // 添加后可 git remote 查看
    }
  }
}

module.exports = Git
